iOS多线程 - 同步线程

比如你去银行存10K,在用ATM存的同时,又使用网银取了10K,如果线程同步做的不好的话, 虽然取10K元的操作会有记录,但是可能不会反映在余额上。如果要解决这样的问题,就需要用到线程同步,线程同步在开发过程中是非常常见的。下面我们以12306为例,尝试解决如何保证一张票不会被不同窗口,不同客户端重复购买。

首先我们来认识一下互斥锁的概念:

互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。

以上是维基百科给出的解释,如需了解更多请 点击这里

简单来讲,互斥锁是为了防止多个线程访问同一个对象、方法、变量、文件等引起的数据错误。

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕, 当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。

互斥锁是同步锁的一种,与它同级的还有递归锁、条件锁、自旋锁。其余几种作者认识并不是很深入,先简单提一下。下面我们使用代码来了解互斥锁:

objc_sync_enter 和 objc_sync_exit

表达式如下:

1
2
3
objc_sync_enter(self)
//需要执行的代码块
objc_sync_exit(self)

其中的 self 代表一把锁,它的传入值是任意一个对象,如果多线程访问同一个资源,那么必须使用同一把锁才能锁住。因为必须使用同一把锁,开发中如果需要加锁,通常我们可以直接使用 self 即可。

下面我们来运用 objc_sync_enterobjc_sync_exit

首先创建一个变量,代表当前车票的数量:

1
var iTicketCount: Int = 10

接下来,我们使用 NSBlockOperation 实例并添加任务,当票数大于 0 时,票数就可以自减 1,直到票数等于 0,

1
2
3
4
5
6
7
8
9
10
11
let saleTicketOne = NSBlockOperation()
saleTicketOne.addExecutionBlock {
while self.iTicketCount > 0 {
objc_sync_enter(self)
self.iTicketCount -= 1
print("当前线程是 \(NSThread.currentThread()) 剩余票数\
objc_sync_exit(self)
}
}

我们知道,NSBlockOperation 本身是不具备开线程的能力的,NSBlockOperationQueue才可以开线程,而 NSBlockOperationQueue 是否可以开线程,又取决于任务的数量,现在我们的任务数只有一个,如果要使用线程同步,必须是两个以上的线程,所以我们再创建一个 NSBlockOperation。将两个任务添加到队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
let saleTicketTwo = NSBlockOperation()
saleTicketTwo.addExecutionBlock {
while self.iTicketCount > 0 {
objc_sync_enter(self)
self.iTicketCount -= 1
print("当前线程是 \(NSThread.currentThread()) 剩余票数\(self.iTicketCount)" )
objc_sync_exit(self)
}
}
let queue = NSOperationQueue()
queue.addOperation(saleTicketOne)
queue.addOperation(saleTicketTwo)

打印日志如下:

1
2
3
4
5
6
7
8
9
10
11
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数9
当前线程是 <NSThread: 0x7b6653c0>{number = 4, name = (null)} 剩余票数8
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数7
当前线程是 <NSThread: 0x7b6653c0>{number = 4, name = (null)} 剩余票数6
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数5
当前线程是 <NSThread: 0x7b6653c0>{number = 4, name = (null)} 剩余票数4
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数3
当前线程是 <NSThread: 0x7b6653c0>{number = 4, name = (null)} 剩余票数2
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数1
当前线程是 <NSThread: 0x7b6653c0>{number = 4, name = (null)} 剩余票数0
当前线程是 <NSThread: 0x7b6635d0>{number = 3, name = (null)} 剩余票数-1

可以看出我们开启了两条线程,使用互斥锁执行同一个任务,结果是两条线程交叉执行这个任务,结果基本符合预期,但是在票数上却有问题,那么造成这个问题的原因是什么呢?

因为这两个线程是并发执行,在票数等于 1 时,一个线程通过 while self.iTicketCount > 0 的判断,然后一个线程也进入了这个判断,前面的进程遇到 objc_sync_enter(self) ,进入加锁执行,输出了 0,然后解锁。另一个进程也进入加锁执行,此时了票数为 0,所以输出了 -1。

如果说因为条件判断通过,所以造成了结果的错误,那么我们将条件判断 while self.iTicketCount > 0 也加入锁的范围内,会怎样呢?

我们将代码改成这样:

1
2
3
4
5
6
7
8
9
let saleTicketOne = NSBlockOperation()
saleTicketOne.addExecutionBlock {
objc_sync_enter(self)
while self.iTicketCount > 0 {
self.iTicketCount -= 1
print("当前线程是 \(NSThread.currentThread()) 剩余票数\
}
objc_sync_exit(self)
}

打印日志如下:

1
2
3
4
5
6
7
8
9
10
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数9
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数8
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数7
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数6
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数5
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数4
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数3
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数2
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数1
当前线程是 <NSThread: 0x7b74f560>{number = 3, name = (null)} 剩余票数0

可以看出,只有一条线程执行任务,那么可以说明,只会有一个线程执行完整个任务后,另一个进程才会执行,但此时它已经没有任务可做了。这样也不对,那么正确做法是怎样的呢?

1
2
3
4
5
6
7
8
9
10
11
let saleTicketTwo = NSBlockOperation()
saleTicketTwo.addExecutionBlock {
while self.iTicketCount > 0 {
objc_sync_enter(self)
if self.iTicketCount > 0 {
self.iTicketCount -= 1
print("当前线程是 \(NSThread.currentThread()) 剩余票数\(self.iTicketCount)" )
objc_sync_exit(self)
}
}
}

我们在 objc_sync_enter(self) 再加入一条判断,if self.iTicketCount > 0 那么输出的结果就没有问题了,可以看到,加锁的位置和条件的判断在同步进程中是非常重要的,如果同步线程的加锁位置和判断条件没有做好,可能会出现这段代码还没有执行,另一个线程中已经执行了,因此造成数据存取的不一致,下面我们来了解一下使用 NSBlock 来做同步线程:

NSLock

首先来看下 NSLock 的表达式:

1
2
3
4
5
6
7
8
public class NSLock : NSObject, NSLocking {
public func tryLock() -> Bool
public func lockBeforeDate(limit: NSDate) -> Bool
@available(iOS 2.0, *)
public var name: String?
}

可以看出,NSLock 是一个类,包含两个实例方法 tryLocklockBeforeDate 以及一个存储属性 name,并且继承了 NSLocking ,我们再来看下 NSLocking 这个父类:

1
2
3
4
5
public protocol NSLocking {
public func lock()
public func unlock()
}

NSLocking 中有两个实例方法,分别是加锁和解锁,关于 NSLock 还有一些其他的属性和方法,大家可以在帮助文档中找到,在此就不一一介绍了。

下面我们使用 NSThreadNSBlock 来实现和上面同样的功能,首先创建两个成员属性:

1
var iTicketCount: Int = 10 //表示当前剩余的票数
1
var myLock: NSLock! //表示一个锁

下面我们将 myLock 进行实例化,创建两个线程并添加任务:

1
2
3
4
5
self.myLock = NSLock()
let threadOne = NSThread(target: self, selector: #selector(ViewController.saleTicket), object: nil)
let threadTwo = NSThread(target: self, selector: #selector(ViewController.saleTicket), object: nil)
threadOne.start()
threadTwo.start()

我们要执行的任务是:

1
2
3
4
5
6
7
8
9
10
11
12
13
func saleTicket() {
while self.iTicketCount > 0 {
self.myLock.lock()
if self.iTicketCount > 0 {
self.iTicketCount -= 1
print("当前线程是 \(NSThread.currentThread()) 剩余票数\(self.iTicketCount)" )
}
self.myLock.unlock()
}

做好条件控制,在需要同步线程的代码上下分别启用 myLock.lock()myLock.unlock() 就可以了。